package info.niwota.js;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.appengine.api.urlfetch.FetchOptions;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;

@SuppressWarnings("serial")
public class CompressServlet extends HttpServlet {
	private static final Logger log = Logger.getLogger(CompressServlet.class
			.getName());
	
	public static final SimpleDateFormat RFC1123_DF = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'");

	private static final Pattern SCRIPT_PATTERN = Pattern.compile("<script\\b[^>]*src=\"([^\"]*?)\"[^>]*>.*?</script>");

	private static final Pattern XSCRIPT_PATTERN = Pattern.compile("<script\\b[^>]*type=\"text/x-packed-javascript\"[^>]*>(.*?)</script>", Pattern.MULTILINE|Pattern.DOTALL);

	private static final Compressor[] compressors = new Compressor[] {
			new NoopCompressor(), new GoogleCompressor(),
			new YUIJsCompressor(), new DojoCompressor(), new DWRCompressor(),
			new JSMinCompressor() };

	private static final int DEFAULT_COMPRESSOR = 0;
	
	private static final String PARAM_COMPRESSOR = "compressor";
	private static final String PARAM_STAT = "stat";
	private static final String PARAM_URI = "uri";

	private static int matchCompressor(String s) {
		final String[] comps = new String[] { "noop", "goog", "yui", "dojo",
				"dwr", "jsmin" };
		if (s == null) {
			return DEFAULT_COMPRESSOR;
		}
		s = s.toLowerCase();
		for (int i = 0; i < comps.length; i++) {
			if (s.startsWith(comps[i])) {
				return i;
			}
		}
		return DEFAULT_COMPRESSOR;
	}

	public void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws IOException, ServletException {
		try {
			if (handle(req, resp)) {
				return;
			}

			handleNotFound(req, resp);
		} catch (Exception e) {
			e.printStackTrace();
			handleError(req, resp, e);
		}
	}

	protected boolean handle(HttpServletRequest req, HttpServletResponse resp)
			throws Exception {
		final String path = req.getRequestURI();
		final String uri = req.getParameter(PARAM_URI);
		final String compressor = req.getParameter(PARAM_COMPRESSOR);
		final Boolean stat = parseBoolean(req.getParameter(PARAM_STAT));
		final String ref = req.getHeader("Referer");
		
		log.finest("path: " + path + " compressor: " + compressor + " stat: " + stat + " uri: " + uri  + " ref: " + ref);

		//min or fallback to min if no referrer for pack
		if (path.equals("/min") || (path.equals("/pack") && ref == null)) {
			Date lastMod = new Date();
			Date expires = new Date();
			byte[] script = getContent(uri, lastMod, expires);
			
			resp.setContentType("text/javascript");
			resp.setDateHeader("Last-Modified", lastMod.getTime());
			resp.setDateHeader("Expires", expires.getTime());
			
			ServletOutputStream sos = resp.getOutputStream();
			CounterOutputStream cos = new CounterOutputStream(sos);

			compress(matchCompressor(compressor), script, stat ? cos : sos);
			
			if (stat) {
				stats(sos, 1, script.length, cos.getCount());
			}
			
			log.fine("min: " + uri);
			return true;
		}
		
		if (path.equals("/pack")) {
			//referrer not null

			StringBuilder sb = new StringBuilder();
			sb.append(" packed compressor: " + compressor);
			sb.append(" stat: " + stat);

			comment(sb.toString(), req, resp);
			
			log.finest("pack: " + uri + " ref: " + ref);
			return true;
		}
		
		if (path.equals("/packed")) {
			if (ref == null) {
				comment("ignored", req, resp);
				return true;
			} 
			
			List<String> list = extractLinks(ref);
			pack(list, stat, compressor, req, resp);
			
			log.finest("packed: " + list + " uri: " + uri + " ref: " + ref);
			return true;
		}
		
		if (path.equals("/xpacked")) {
			if (ref == null && uri == null) {
				Uri u = new Uri(req.getRequestURL().toString());
				u.setParameter(PARAM_COMPRESSOR, compressor);
				u.setParameter(PARAM_STAT, stat + "");
				String s = "var link='" + u.toString() + "'+'&uri='+window.location.href;var s=document.createElement('script');s.setAttribute('type','text/javascript');s.setAttribute('src',link);document.getElementsByTagName('head')[0].appendChild(s);";
				callback(s, req, resp);
				
				System.out.println(s);
				return true;
			}
			
			List<String> list = extractXLinks(uri == null ? ref : uri);
			pack(list, stat, compressor, req, resp);
			
			log.finest("xpacked: "+ list + " uri: " + uri + " ref: " + ref);
			return true;
		}

		return false;
	}
	
	private void callback(String script, HttpServletRequest req, HttpServletResponse resp) throws IOException {
		resp.setContentType("text/javascript");
		ServletOutputStream sos = resp.getOutputStream();
		sos.print("(function () {");
		sos.print(script);
		sos.print("})();");
		sos.flush();
	}

	private void comment(String msg, HttpServletRequest req, HttpServletResponse resp) throws IOException {
		resp.setContentType("text/javascript");
		ServletOutputStream sos = resp.getOutputStream();
		sos.print("/* ");
		sos.print(msg);
		sos.print(" */");
		sos.flush();
	}

	private void pack(List<String> list, boolean stat, String compressor, HttpServletRequest req, HttpServletResponse resp) throws Exception {
		ServletOutputStream sos = resp.getOutputStream();
		CounterOutputStream cos = stat ? new CounterOutputStream(sos) : null;
		int which = matchCompressor(compressor);
		Date lastMod = new Date();
		Date expires = new Date();
		int size = 0;
		int files = 0;
		for (Iterator<String> it = list.iterator(); it.hasNext();) {
			files++;
			byte[] script = getContent(it.next(), lastMod, expires);
			if (script != null) {
				size += script.length;
				compress(which, script, stat ? cos : sos);
			}
		}
		
		resp.setContentType("text/javascript");
		resp.setDateHeader("Last-Modified", lastMod.getTime());
		resp.setDateHeader("Expires", expires.getTime());
		
		if (stat) {
			stats(sos, files, size, cos.getCount());
		}
	}

	private boolean parseBoolean(String s) {
		return (s != null && s.equals("true"));
	}

	private void stats(ServletOutputStream os, int file, int total, int count) throws IOException {
		os.println();
		os.print("/*");
		os.print(" files: " + file);
		os.print(" original: " + total);
		os.print(" compressed: " + count);
		os.print(" ratio: " + ((float)count/(float)total));
		os.print(" */");
	}

	private void compress(int which, byte[] script, OutputStream out) {
		try {
			compressors[which].compress(script, out);
			out.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void handleError(HttpServletRequest req, HttpServletResponse resp,
			Throwable e) throws IOException {
		final String msg = e.getMessage();
		resp.setContentType("text/javascript");
		resp.getWriter().println("/* " + msg + " */");
	}

	private void handleNotFound(HttpServletRequest req, HttpServletResponse resp)
			throws IOException {
		final String uri = req.getRequestURI();
		resp.setContentType("text/javascript");
		resp.getWriter().println("/* Not found */" + uri);
	}

	private byte[] getContent(String link, Date lastMod, Date expires) throws Exception {
		URL url = new URL(link);
		URLFetchService ufs = URLFetchServiceFactory.getURLFetchService();
		HTTPResponse res = ufs.fetch(new HTTPRequest(url, HTTPMethod.GET,
				FetchOptions.Builder.disallowTruncate().followRedirects()));

		for (HTTPHeader h: res.getHeaders()) {
			if (h.getName().equals("Last-Modified")) {
				Date d = parseRFC1123(h.getValue());
				if (lastMod != null && d.after(lastMod)) {
					lastMod.setTime(d.getTime());
				}
			} else if (h.getName().equals("Expires")) {
				Date d = parseRFC1123(h.getValue());
				if (expires != null && d.before(expires)) {
					expires.setTime(d.getTime());
				}
			}
		}
		
		byte[] c = res.getContent();
		return c;
	}
	
	/**
	 * Return page head content
	 * 
	 * @param link
	 * @param crlf
	 * @return
	 * @throws IOException
	 */
	private String getPartialContent(String link, boolean crlf) throws IOException {
		log.finest("getPartialContent: " + link);
		
		BufferedReader r = null;
		try {
			URL url = new URL(link);
			r = new BufferedReader(new InputStreamReader(url.openStream()));

			StringBuffer sb = new StringBuffer();
			String line = null;
			while ((line = r.readLine()) != null) {
				sb.append(line);
				if (crlf) {
					sb.append("\n");
				}
				if (line.indexOf("</head>") != -1) {
					break;
				}
			}

			return sb.toString();
		} finally {
			r.close();
		}
	}

	private List<String> extractLinks(String link) throws Exception {
		List<String> links = new ArrayList<String>();
		String c = getPartialContent(link, false);
		Matcher m = SCRIPT_PATTERN.matcher(c);
		while (m.find()) {
			String s = m.group(1);
			Uri u = new Uri(s);
			String p = u.getPath();
			if (p != null && p.equals("/pack")) {
				links.add(u.getParameter(PARAM_URI));
			}
		}
		return links;
	}
	
	private List<String> extractXLinks(String link) throws Exception {
		String c = getPartialContent(link, true);
		List<String> links = new ArrayList<String>();
		Matcher m = XSCRIPT_PATTERN.matcher(c);
		while (m.find()) {
			String s = m.group(1);
			String[] ta = s.split("\n");
			for (int i = 0; i < ta.length; i++) {
				String t = ta[i].trim();
				if (!t.equals("")) {
					links.add(t);
				}
			}
		}
		return links;
	}

	protected int parseInt(String s, int defval) {
		try {
			return Integer.parseInt(s);
		} catch (Exception e) {

		}
		return defval;
	}

	protected static String formatRFC1123(long date) {
		String s = RFC1123_DF.format(new Date(date));
		return s;
	}
	
	protected static Date parseRFC1123(String s) throws ParseException {
		return RFC1123_DF.parse(s);
	}
}
